package com.tong.hao.common.configuration.redis;

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.cache.CacheProperties;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.RedisSerializer;

/**
 * @author shihao
 * @description Redis配置, 参见 RedisAutoConfiguration
 * @see org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration
 *
 * <pre>
 * 1. 从RedisTemplate中的afterPropertiesSet()方法中可以看出RedisTemplate默认序列化和反序列化方式为JDK。
 * 1.1 不推荐使用JDK序列化方式的原因：
 *      1). jdk序列化对象到redis的value是乱码(好像是十六进制)，对于使用来说没啥问题，but，对于开发人员排查问题就很恶心了。
 *      2). 对于我们需要序列化到Redis中的所有pojo类都需要实现java.io.Serializable,并且在pojo类中一定要有静态变量serialVersionUID,
 *          如果pojo类中没有serialVersionUID,那么序列化或者反序列化就有可能会失败,抛出NotSerializableException异常。
 * 1.2 NotSerializableException异常问题描述：
 *     当redis序列化方式为JDK时,必须要实现java.io.Serializable,并且在pojo类中一定要有静态变量serialVersionUID。
 *     如果没有开发人员没有手动写serialVersionUID,java也会自动给我们生成一个值,这个变量的值是根据pojo类的属性计算出来的。
 *
 *     举栗子：
 *          开发人员没有写静态变量serialVersionUID。
 *          1). 序列化时,java会计算出一个serialVersionUID,
 *          2). 当序列化完成后,我们给pojo类随便添加一个非静态属性。
 *          3). 当反序列化时就会抛出NotSerializableException异常，因为反序列化时,pojo类多了一个属性,
 *              就会导致反序列化时java计算的serialVersionUID与序列化时计算的值不一样,就抛出异常了。
 *     【ps】：所以说，我们一般都在pojo中写 private static final long serialVersionUID = 1L;
 * 1.3 jdk序列化、json序列法方式，对于静态变量无法序列化，这个一定要注意。
 * </pre>
 */
@Configuration
@ConditionalOnProperty(value = "hao.redis.config.enabled", havingValue = "true", matchIfMissing = true)
@Slf4j
public class HaoRedisConfig {

    /**
     * Spring Cache 配置文件
     */
    private final CacheProperties cacheProperties;

    HaoRedisConfig(CacheProperties cacheProperties) {
        this.cacheProperties = cacheProperties;
    }

    /**
     * RedisTemplate配置
     */
    @Bean(name = "redisTemplate")
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        log.info("RedisTemplate配置");
        // 创建 RedisTemplate 对象
        RedisTemplate<String, Object> template = new RedisTemplate<>();

        // 设置 RedisConnection 工厂 (RedisConnection 就是实现多种 Java Redis 客户端接入的秘密工厂)
        template.setConnectionFactory(redisConnectionFactory);
        // 序列化key：使用 string 序列化
        template.setKeySerializer(RedisSerializer.string());
        template.setHashKeySerializer(RedisSerializer.string());
        // 序列化value：使用 json 序列化 (使用 Jackson 序列化)
        template.setValueSerializer(RedisSerializer.json());
        template.setHashValueSerializer(RedisSerializer.json());
        // 不使用 spring data redis 默认的序列化方式(JdkSerializationRedisSerializer, JDK序列化)
        template.setEnableDefaultSerializer(false);

        return template;
    }

    /**
     * StringRedisTemplate配置
     */
    @Bean(name = "stringRedisTemplate")
    public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) {
        log.info("StringRedisTemplate配置");
        // 创建 StringRedisTemplate 对象
        StringRedisTemplate template = new StringRedisTemplate();

        // 设置 RedisConnection 工厂 (RedisConnection就是 实现多种 Java Redis 客户端接入的秘密工厂)
        template.setConnectionFactory(redisConnectionFactory);
        // 不使用 spring data redis 默认的序列化方式(JdkSerializationRedisSerializer, JDK序列化)
        template.setEnableDefaultSerializer(false);

        return template;
    }

    /**
     * 这个配置是给 @org.springframework.cache.annotation.Cacheable注解,也就是SpringCache用的。
     * Spring Cache + Redis 配置，为 RedisCacheManager 做准备。
     * 具体参见
     *
     * @see org.springframework.boot.autoconfigure.cache.RedisCacheConfiguration
     * 的 cacheManager 方法
     * 默认情况下，RedisCacheConfiguration的value序列化方式为JDK(RedisTemplate也是默认也是jdk序列化)
     */
    @Bean
    public RedisCacheConfiguration redisCacheConfiguration() {
        Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(
                Object.class);
        ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        jackson2JsonRedisSerializer.setObjectMapper(om);

        // 设置 RedisCache 的 value 序列化方式
        RedisCacheConfiguration redisCacheConfiguration =
                RedisCacheConfiguration.defaultCacheConfig()
                        .serializeValuesWith(RedisSerializationContext
                                .SerializationPair
                                .fromSerializer((jackson2JsonRedisSerializer)));

        // 获取 RedisCache 配置,这个需要我们开发人员配置
        CacheProperties.Redis redisProperties = this.cacheProperties.getRedis();
        if (redisProperties.getTimeToLive() != null) {
            redisCacheConfiguration = redisCacheConfiguration.entryTtl(redisProperties.getTimeToLive());
        }

        if (redisProperties.getKeyPrefix() != null) {
            redisCacheConfiguration = redisCacheConfiguration.prefixKeysWith(redisProperties.getKeyPrefix());
        }

        if (!redisProperties.isCacheNullValues()) {
            redisCacheConfiguration = redisCacheConfiguration.disableCachingNullValues();
        }

        if (!redisProperties.isUseKeyPrefix()) {
            redisCacheConfiguration = redisCacheConfiguration.disableKeyPrefix();
        }

        return redisCacheConfiguration;
    }

}
